package com.eischet.lookup.web;

import org.apache.log4j.Logger;

import org.apache.lucene.search.Filter;
import org.apache.lucene.search.TermRangeFilter;
import org.springframework.beans.factory.annotation.Required;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;

import static com.eischet.lookup.Functional.*;
import static com.eischet.lookup.StringUtilities.*;

/**
 * The Lookup web application, which allows searching the index.
 * 
 * Muhamed ist der dopeste
 */
public class WebApplication {
    public static final int[] VALID_PAGE_SIZES = { 10, 20, 50, 100 };
    private static final Logger logger = Logger.getLogger(WebApplication.class);
    private SearchIndex searchIndex;
    private long mostRecentWatermark;

    /**
     * configures the search index that will be used
     * @param searchIndex the search index object
     */
    @Required
    public void setSearchIndex(SearchIndex searchIndex) {
        this.searchIndex = searchIndex;
    }

    /**
     * calculates how much time has passed since the time stamp startTime
     * @param startTime     a time stamp (such as obtained by System.getCurrentTimeMillis()
     * @return              the time difference, in seconds, as a String
     */
    private String getElapsedTime(long startTime) {
        long endTime = System.currentTimeMillis();
        long diff = endTime - startTime;
        if (diff == 0) {
            return "0.0";
        } else {
            return String.valueOf((endTime - startTime) / 1000.0d);
        }
    }

    /**
     * retrieves a Date argument (in String form) from the servlet request parameters
     *
     * @param request       the servlet request
     * @param dateKey       the date parameter name/key
     * @param timeKey       the time parameter name/key
     * @return              the combined date/time as a String
     */
    private static String getDateArgument(HttpServletRequest request, String dateKey, String timeKey) {
        String date = trimAndRemoveNulls(request.getParameter(dateKey));
        String time = trimAndRemoveNulls(request.getParameter(timeKey));
        if (!date.isEmpty() && !time.isEmpty()) {
            return String.format("%s %s", date, time);
        } else {
            return date;
        }
    }

    /**
     * returns a query argument, as an int.
     * <br>
     * If the minimum is larger than _default, it effectively overrides the _default value!
     *
     * @param request       the servlet request
     * @param key           the parameter name/key
     * @param _default      the default value to use instead of malformed or missing values
     * @param minimum       a minimum value to accept
     * @return              the request parameter, or the _default, but at least the minimum
     */
    private static int getIntegerArgument(HttpServletRequest request, String key, int _default, int minimum) {
        String stringValue = request.getParameter(key);
        if (stringValue == null) {
            return _default;
        }
        try {
            int value = Integer.parseInt(stringValue, 10);
            return Math.max(value, minimum);
        } catch (NumberFormatException ex) {
            logger.warn(String.format("number format error in %s='%s'", key, stringValue));
            return _default;
        }
    }

    /**
     * Parses GET-style query parameters. In contrast to the Java built-in functionality (request.getParameter()),
     * this method does in fact handle UTF-8 Strings correctly.
     *
     * @param queryString   the query String, as obtained from the servlet request
     * @return              a Map of argument keys/names and values
     */
    Map<String, String> getArgs(String queryString) {
        String qs = queryString;
        if (logger.isDebugEnabled()) {
            logger.debug("request.queryString: " + qs);
        }
        qs = urlDecode(qs);
        if (logger.isDebugEnabled()) {
            logger.info("request.queryString decoded: " + qs);
        }
        Map<String, String> args = new HashMap<String, String>();
        for (String parameter: splitString(qs, "&")) {
            List<String> part = splitString(parameter, "=", 1);
            String key = firstOf(part);
            String value = lastOf(part);
            args.put(key, value);
        }
        return args;
    }

    /**
     * The dispatcher method is where all HTTP requests are routed through.
     * This method is, effectively, the core of Lookup's web application.
     *
     * @param request       a serlvet request
     * @param response      a servlet response
     */
    public void dispatch(HttpServletRequest request, HttpServletResponse response) {
        Map<String, String> args = getArgs(request.getQueryString());
        LuceneQuery query = null;
        long startTime = System.currentTimeMillis();
        HttpSession session = request.getSession(true);
        List<String> domainAndUserName = splitString(request.getUserPrincipal().getName(), "\\\\");
        String loginDomain = firstOf(domainAndUserName).toUpperCase();
        String loginName = lastOf(domainAndUserName).toUpperCase();
        String userQuery = args.get("q");
        if (logger.isDebugEnabled()) {
            logger.debug(String.format("userQuery='%s'", userQuery));
        }
        String type = args.get("t");
        int startIndex = getIntegerArgument(request, "s", 0, 0);
        int pageSize = getIntegerArgument(request, "p", 10, 10);
        Pages page = Pages.getPageFor(request.getParameter("do"));
        String fromDate = getDateArgument(request, "fromdate", "fromtime");
        String toDate = getDateArgument(request, "todate", "totime");
        String handle = args.get("h"); // for the raw viewer
        String error = null;
        List<SearchResult> results = Collections.emptyList();
        Map<String, Long> statistics = Collections.emptyMap();
        if (page.equals(Pages.Results) && (userQuery==null || userQuery.isEmpty())) {
            page = Pages.SearchForm;
        }
        if (logger.isDebugEnabled()) {
            logger.debug("rendering page: " + page);
        }
        switch (page) {
           case SearchForm:
                break;
            case Results:
                Filter dateFilter = getDateFilter(fromDate, toDate);
                String constrainedQuery = addConstraintsToQuery(userQuery, type);
                try {
                    query = searchIndex.query(loginName, session, constrainedQuery, dateFilter);
                    results = query.getHits(startIndex, pageSize);
                    if (logger.isDebugEnabled()) {
                        logger.debug("found search results: " + results.size());
                    }
                    logQuery(constrainedQuery, query.getNumberOfHits(), loginName);
                } catch (Exception e) {
                    logger.warn("query error", e);
                    error = e.getMessage();
                } finally {
                    query.close();
                }
                break;
            case About:
                statistics = getStatistics(loginName, session);
                break;
            case Help:
                break;
            case RawView:
                try {
                    results = searchIndex.searchForHandle(handle, loginName, session);
                } catch (Exception e) {
                    logger.warn("raw view query error", e);
                    error = e.getMessage();
                }
                break;
        }
        request.setAttribute("mostRecentWatermark", formatDate(mostRecentWatermark > 0 ? new Date(mostRecentWatermark*1000) : null));
        request.setAttribute("statistics", statistics);
        request.setAttribute("elapsedTime", getElapsedTime(startTime));
        request.setAttribute("userQuery", escapeForHtml(userQuery));
        request.setAttribute("type", type);
        request.setAttribute("startIndex", startIndex);
        request.setAttribute("pageSize", pageSize);
        request.setAttribute("page", page);
        request.setAttribute("fromDate", escapeForHtml(fromDate));
        request.setAttribute("toDate", escapeForHtml(toDate));
        request.setAttribute("results", results);
        request.setAttribute("numberOfHits", query != null ? query.getNumberOfHits() : 0);
        request.setAttribute("error", escapeForHtml(error));
        request.setAttribute("searchIndex", searchIndex);
        request.setAttribute("loginName", escapeForHtml(loginName));
        request.setAttribute("loginDomain", escapeForHtml(loginDomain));
        request.setAttribute("lucene", query);
        request.setAttribute("requestedHandle", escapeForHtml(handle));
        try {
            response.setContentType("text/html; charset=UTF-8");
            RequestDispatcher dispatcher = request.getRequestDispatcher("index.jsp");
            dispatcher.forward(request, response);
        } catch (ServletException e) {
            throw new RuntimeException("servlet exception forwarding to view", e);
        } catch (IOException e) {
            throw new RuntimeException("IO exception forwarding to view", e);
        }
    }

    private String formatDate(Date date) {
        if (date == null) {
            return null;
        }
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
        return sdf.format(date);
    }

    /**
     * Logs query results.
     * TODO: enhance to log to a separate log file or to a database table.
     *
     * @param userQuery     the query that was run against the index
     * @param numberOfHits  the number of hits the query returned
     */
    private void logQuery(String userQuery, int numberOfHits, String loginName) {
        logger.info(String.format("query results: %s;%s;%s", userQuery, numberOfHits, loginName));
    }

    /**
     * Retrieves statistics, as a map of object types and counts.
     * NOTE: When upgrading to Lucene 3.1 in the future, you should use that version's new hit counter which
     * does not need to retrieve all the documents but merely counts them.
     *
     * @param loginName     the user's login name, used to determine access rights
     * @param session       the user's session, also used for determining access rights
     * @return              a Map of statistics, keyed by object types
     */
    private Map<String, Long> getStatistics(String loginName, HttpSession session) {
        Map<String, Long> statistics = new HashMap<String, Long>();
        long total = 0;
        for (SearchableCategory category: searchIndex.getSearchableCategories()) {
            if (category.isSelectable()) {
                long count = getObjectCount(loginName, session, category.getName());
                statistics.put(category.getName(), count);
                total += count;
            }
            for (SearchableType type: category.getSearchableTypes()) {
                long count = getObjectCount(loginName, session, type.getName());
                statistics.put(type.getName(), count);
                total += count;
            }
        }
        return statistics;
    }

    /**
     * Retrieves statistics [i.e., COUNT(*)], from the lucene Index for a named searchable object type.
     *
     * @param loginName     user's login name
     * @param session       user's web session
     * @param name          name of the search object to count
     * @return              the number of search objects <i>which the user can see</i>
     */
    private long getObjectCount(String loginName, HttpSession session, String name) {
        LuceneQuery q;
        try {
            q = searchIndex.query(loginName, session, String.format("type:%s", name), null);
            q.getHits(1, 1);
            return q.getNumberOfHits();
        } catch (IndexInaccessibleError e) {
            logger.warn(String.format("index inaccessible while counting objects of type '%s'", name));
            return 0;
        } catch (Exception e) {
            logger.error(String.format("query error while counting objects of type '%s'", name), e);
            return 0;
        }
    }

    /**
     * Builds a date filter between the given two dates, which must be Strings in YYYYMMDDHHMMSS format.
     * The "magic" field "updatets" is used for this, which is configured in update.xml.
     *
     * @param fromDate      starting date
     * @param toDate        ending date
     * @return              a Lucene Filter object that only finds hits between those dates
     */
    private Filter getDateFilter(String fromDate, String toDate) {
        if (logger.isDebugEnabled()) {
            logger.debug(String.format("getDateFilter(fromDate='%s', toDate='%s')", fromDate, toDate));
        }
        Filter filter = null;
        if ((fromDate != null && !fromDate.isEmpty()) || (toDate != null && !toDate.isEmpty())) {
            String qFromDate = numbersOnly(fromDate);
            String qToDate = numbersOnly(toDate);
            if (!qFromDate.isEmpty() && !qToDate.isEmpty() && largerThan(qFromDate, qToDate)) {
                String temp = qFromDate;
                qFromDate = qToDate;
                qToDate = temp;
            }
            if (qFromDate.isEmpty()) qFromDate = "197601100000";
            if (qToDate.isEmpty()) qToDate = "203001010000";
            filter = new TermRangeFilter("updatets", qFromDate, qToDate, true, true);
            logger.info(String.format("getDateFilter(): filter=%s", filter));
        }
        return filter;
    }

    /**
     * add query constraints to the user-entered query
     *
     * @param userQuery     the user-entered query
     * @param type          the value from the type dropdown selection
     * @return              a combined query
     */
    private String addConstraintsToQuery(String userQuery, String type) {
        if (logger.isDebugEnabled()) {
            logger.debug(String.format("addConstraintsToQuery(userQuery='%s', type='%s')", userQuery, type));
        }
        if (type != null && !type.isEmpty() && !"any".equalsIgnoreCase(type)) {
            userQuery = String.format("type:%s AND ( %s )", type, userQuery);
            if (logger.isDebugEnabled()) {
                logger.debug(String.format("addConstraints: rewrote userQuery='%s'", userQuery));
            }
        }
        return userQuery;
    }

    /**
     * @return a String representation of this object
     */
    @Override
    public String toString() {
        return String.format("WebApplication(%s)", super.toString());
    }

    public void setMostRecentWatermark(long mostRecentWatermark) {
        this.mostRecentWatermark = mostRecentWatermark;
    }
}
